✨ Add power modifier to MLIR#1603
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
f017b29 to
4964ed0
Compare
|
|
||
| // Move supporting ops (constants, arithmetic) out of the body so their | ||
| // Values are accessible from outside and survive PowOp erasure. | ||
| for (auto& bodyOp : llvm::make_early_inc_range(*op.getBody())) { |
There was a problem hiding this comment.
Because inv creates a negf operation inside its body we must move out these operations when we want to apply gate specific canonicalizations. Currently I do this here unconditionally - which feels pretty wrong. Isnt there a way for hoisting up operations which can/should be moved out of the body? Should maybe inv and pow run such a hositing up thing?
There was a problem hiding this comment.
I am pretty sure that these operations are automatically hoisted up as part of canonicalization.
I remember this being problematic for nested Control and Inverse modifiers already. But we never had to handle that explicitly.
There was a problem hiding this comment.
Code for inv { pow (2) { rx(0.123) }}:
module {
func.func @testPow() {
%q0_0 = qco.alloc : !qco.qubit
%q0_1 = qco.inv (%a = %q0_0){
%a_1 = qco.pow (2.000000e+00) (%b = %a) {
%c_0 = arith.constant 0.123 : f64
%b_1 = qco.rx(%c_0) %b : !qco.qubit -> !qco.qubit
qco.yield %b_1
} : {!qco.qubit} -> {!qco.qubit}
qco.yield %a_1
} : {!qco.qubit} -> {!qco.qubit}
qco.sink %q0_1 : !qco.qubit
return
}
}will lead to this state (swap pow and inv and then do ReplaceWithKnownGates for inv and rx:
module {
func.func @testPow() {
%cst = arith.constant 1.230000e-01 : f64
%0 = qco.alloc : !qco.qubit
%1 = qco.pow(2.000000e+00) (%arg0 = %0) {
%2 = arith.negf %cst : f64
%3 = qco.rx(%2) %arg0 : !qco.qubit -> !qco.qubit
qco.yield %3
} : {!qco.qubit} -> {!qco.qubit}
qco.sink %1 : !qco.qubit
return
}
}If %2 is not explicitly moved out of the pow modifier it is not possible to merge pow and rx with the known canonicalization. So leaving the code out leads to segfault (ask me how I know lol).
There was a problem hiding this comment.
So the segfault possibly occurs, because we want to create pow(r) { rx(θ) } → rx(r*θ) with r = %2. %2 only exists inside the body of pow which gets replaced/deleted when doing replaceOpWithNewOp, creating a dangling reference to %2.
There was a problem hiding this comment.
Hm. This very much depends on the sequence of the individual canonicalization passes.
Because there definitely is a canonicalization that will pull out the %2 = arith.negf %cst : f64 and fold it into the %cst = arith.constant 1.230000e-01 : f64 statement, which would result in the elimination of the arith.negf instruction and would also eliminate the segfault cause.
There has to be a way to address this. Either by making the canonicalization more robust (e.g., by moving the body of the original pow operation to the outside and then performing the manipulation) or by marking the canonicalization only as valid whenever the body of the operation only consists of the UnitaryOperation (and the yield).
The first solution seems to be more desirable to me.
This is kind of like inlining plus some additional manipulation.
I am pretty sure https://mlir.llvm.org/doxygen/classmlir_1_1RewriterBase.html#a4307b18bc25cec55349236d556cc89bd should do the trick here.
The canonicalization is basically inlining the block and adding a arith.mulf (or whatever the instruction is called) on the rotation angle argument before replacing the rotation angle argument with the new rotation angle.
|
Ok, I discovered another problem, consider this example:
My idea then: the then and then The only thing im not to sure about, whether the Concrete implementation: instead of emiting 2 operations, the canonicalization creates an inner operation and a preamble.
I dont know if creating a whole interface for that is premature, i believe only the power modifier emits 2-operation-canonicalizations so the whole thing could stay within power modifier. edit: maybe, when keeping it general it would make sense having this as an interface, so that other operations could be moved out as well (like arithmetic operations which e.g. |
2375c9d to
993a3eb
Compare
faeba24 to
05c9335
Compare
|
While implementing the QC<->QCO conversion tests, I noticed some friction: |
91700af to
b1759c7
Compare
|
Ok, commits cleaned up, linting and coverage succeeds - i think this is ready for a first look @DRovara @burgholzer |
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (23)
💤 Files with no reviewable changes (9)
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a PowOp modifier across QC and QCO: TableGen op declarations, builder APIs, bidirectional conversions, canonicalization/folding and unitary-matrix support, math helpers, and broad unit-test/program-builder coverage for pow semantics. ChangesPowOp modifier
Sequence Diagram(s)sequenceDiagram
participant User
participant QCBuilder as QC ProgramBuilder
participant QCPowOp as qc::PowOp
participant Converter as QC↔QCO Converter
participant Canon as Canonicalizer
User->>QCBuilder: pow(exponent, body)
QCBuilder->>QCPowOp: create with exponent and body callback
QCPowOp->>QCPowOp: verify structure (exponent constant, single unitary op)
alt convert to QCO
Converter->>Converter: resolve mapped qubits
Converter->>Converter: create qco.pow, move body region
Converter->>Converter: add entry block qubit args
end
QCPowOp->>Canon: enqueue for canonicalization
Canon->>Canon: apply patterns (inline pow(1), erase pow(0))
Canon->>Canon: merge nested pow, move ctrl outside
Canon->>Canon: fold into gate (scale params, decompose)
alt folding succeeds
Canon->>User: replace with simplified gates/parameters
else folding inapplicable
Canon->>User: leave PowOp unchanged
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify code
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 markdownlint-cli2 (0.22.1)CHANGELOG.mdmarkdownlint-cli2 v0.22.1 (markdownlint v0.40.0) Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (2)
mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp (1)
68-76:⚠️ Potential issue | 🔴 CriticalThe rewrite set is broader than the body invariant it actually preserves.
verify()admits helper ops before the body unitary, butNegPowToInvPowandMoveCtrlOutsideclone only the unitary,MergeNestedPowonly retargets the moved op’s qubit operands, and the identity-producing branches replacepowwith plain pass-through values. With body-localarith.*or captured values, that can dangle defs after replacement; with a nested parent modifier, it can leave the parent region with no unitary at all. Either tightenverify()back to the single-unitary form, or preserve/hoist the full prologue and rewrite enclosing modifiers in the same pattern.Also applies to: 158-169, 183-199, 218-221, 244-259, 284-289, 482-499, 596-655
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 68 - 76, The rewrite patterns (NegPowToInvPow, MoveCtrlOutside, MergeNestedPow, and functions like tryReplaceWithNamedPhaseGate operating on PowOp) assume a single-unitary body but verify() currently allows helper ops before the unitary; this leads to dangling defs or removal of the unitary while leaving prologue helpers behind. Fix by either tightening verify() on PowOp to require exactly one unitary op in the body (no prologue helpers), or update each rewrite (NegPowToInvPow, MoveCtrlOutside, MergeNestedPow and tryReplaceWithNamedPhaseGate) to preserve/hoist the full prologue: when cloning or retargeting the unitary, also clone or move any preceding helper ops and update their uses, ensure replacements replace the entire region (including helpers) or remap all results/operands so no defs are left dangling, and update operand/region remapping logic to maintain the same external SSA edges for PowOp.mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)
60-68:⚠️ Potential issue | 🔴 CriticalThese rewrites assume a stricter body invariant than
verify()enforces.
verify()currently permits arbitrary non-unitary ops before the body unitary, but the structural rewrites only preservegetBodyUnitary(), and some folds erasePowOpoutright. With body-localarith.*defs, that can dangle operands after replacement; with a nested parent modifier, it can leave the parent body with no unitary at all. Either tighten the body back tounitary + yield, or hoist/inline the full prologue and rewrite the enclosing modifier atomically.Based on learnings,
InvOp::verifyintentionally restricts the body region to exactly two operations (one unitary + yield).Also applies to: 149-158, 170-173, 188-190, 204-207, 230-235, 431-450, 498-517
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@mlir/include/mlir/Dialect/QC/IR/QCOps.td`:
- Around line 1039-1047: The target/control accessors for the pow op are
inconsistent: getNumTargets() and getTarget(size_t) use overall qubit semantics
while getTargets() delegates to getBodyUnitary(), causing mismatched behavior;
update the pow op's accessors to mirror the InvOp pattern by delegating
target/control-related calls to getBodyUnitary() instead of using
getNumQubits()/getQubit(), i.e., make getNumTargets() call
getBodyUnitary().getNumTargets(), make getTarget(size_t i) call
getBodyUnitary().getTarget(i), and ensure
getNumControls()/getControls()/getControl() follow the same body-unitary
delegation so all UnitaryInterface accessors are consistent with
getBodyUnitary().
In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp`:
- Around line 71-75: The current replacement only clones
innerPow.getBodyUnitary().getOperation(), losing any non-terminator preamble ops
(constants/arithmetic) that feed the unitary; update the replacement passed to
rewriter.replaceOpWithNewOp<PowOp> / InvOp::create to iterate over
innerPow.getBody() and clone every non-terminator operation (preserving original
order) into the new region instead of cloning only innerPow.getBodyUnitary(), so
values used by the unitary remain available after MovePowOutside transforms.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 173-201: The NegPowToInvPow rewrite flips sign of the exponent by
replacing pow(p){...} with pow(-p){inv{...}}, which yields conjugated
eigenphases for operators that have eigenvalue -1 and thus produces incorrect
results; modify NegPowToInvPow (the matchAndRewrite for PowOp using
getExponentValue and creating an InvOp) to only apply when the operand unitary's
spectrum is guaranteed not to contain −1 (e.g., obtain the unitary via
getUnitaryMatrix() or a new op method and check no eigenvalue is within an
epsilon of -1), and otherwise skip the rewrite (return failure); alternatively
remove the NegPowToInvPow pattern entirely if no safe spectral test is
available.
In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp`:
- Around line 91-109: Add tests that exercise fractional powers of RXX to hit
the eigendecomposition/branch logic: create additional test cases in
QCOMatrixTest (e.g., new TEST_F methods or extend PowRxxOpMatrix) that build a
module with powRxx at fractional exponents like 0.5 and -0.5 (use the same
QCOProgramBuilder entry used for powRxx but with the power changed), retrieve
the PowOp via func::FuncOp and PowOp/getUnitaryMatrix(), compute the expected
matrix via dd::opToTwoQubitGateMatrix(qc::OpType::RXX, {theta * power}) into an
Eigen::Matrix4cd, and assert matrix->isApprox(eigenDefinition) for both 0.5 and
-0.5 to validate principal-branch/eigendecomposition behavior.
In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp`:
- Around line 893-894: The test case name string in the QCOTestCase entry is
incorrect: change QCOTestCase{"TdgThenS", MQT_NAMED_BUILDER(tdgThenT),
MQT_NAMED_BUILDER(emptyQCO)} so the human-readable name matches the builder by
renaming "TdgThenS" to "TdgThenT" (to align with the tdgThenT builder and
emptyQCO), ensuring test failure messages are not misleading.
In `@mlir/unittests/programs/qc_programs.cpp`:
- Around line 311-315: The global-phase sign is inverted in the reference
decompositions: in powThirdXRef change the gphase argument from -1.0/3.0 *
std::numbers::pi / 2.0 to +1.0/3.0 * std::numbers::pi / 2.0 so the reference
circuit matches the U/R* convention (i.e., X = gphase(+π/2)·rx(π)); apply the
same flip of sign for the gphase calls in the other reference functions
mentioned (the blocks at 357-361, 733-737, 789-793) so they use positive phase
corrections consistent with GPhaseOp(inputPhase - outPhase).
---
Duplicate comments:
In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 68-76: The rewrite patterns (NegPowToInvPow, MoveCtrlOutside,
MergeNestedPow, and functions like tryReplaceWithNamedPhaseGate operating on
PowOp) assume a single-unitary body but verify() currently allows helper ops
before the unitary; this leads to dangling defs or removal of the unitary while
leaving prologue helpers behind. Fix by either tightening verify() on PowOp to
require exactly one unitary op in the body (no prologue helpers), or update each
rewrite (NegPowToInvPow, MoveCtrlOutside, MergeNestedPow and
tryReplaceWithNamedPhaseGate) to preserve/hoist the full prologue: when cloning
or retargeting the unitary, also clone or move any preceding helper ops and
update their uses, ensure replacements replace the entire region (including
helpers) or remap all results/operands so no defs are left dangling, and update
operand/region remapping logic to maintain the same external SSA edges for
PowOp.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e7ba28cd-bad0-4888-8e3a-03526f96e57b
📒 Files selected for processing (22)
mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.hmlir/include/mlir/Dialect/QC/IR/QCOps.tdmlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.hmlir/include/mlir/Dialect/QCO/IR/QCOOps.tdmlir/include/mlir/Dialect/Utils/Utils.hmlir/lib/Conversion/QCOToQC/QCOToQC.cppmlir/lib/Conversion/QCToQCO/QCToQCO.cppmlir/lib/Dialect/QC/Builder/QCProgramBuilder.cppmlir/lib/Dialect/QC/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QC/IR/Modifiers/PowOp.cppmlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cppmlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cppmlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cppmlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cppmlir/unittests/Dialect/QC/IR/test_qc_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cppmlir/unittests/programs/qc_programs.cppmlir/unittests/programs/qc_programs.hmlir/unittests/programs/qco_programs.cppmlir/unittests/programs/qco_programs.h
The problem with just invoking the conversion itself is that the verification becomes very hard at that point since canonicalization has not been run. Constants might be in different places. Some canonicalizations on classical constructs may not have been applied, etc. |
Hm. This one is tricky.
Technical nitpick, but these operations do not operate on the same qubit since the gphase operation has no target qubits.
This is definitely valid as it is also one of the existing canonicalizations for the CtrlOp when that op contains a GphaseOp. Before commenting on the proposal below, a couple of observations: --> --> --> At this stage, the real problem appears, because the The easy way out here is to not permit such PowOp rewrites in CtrlOp modifiers.
I believe I like the general direction of this, but I would not want to create that complexity as part of this PR. Let's rather get a working solution here for the majority of the cases and live with the fact that some constructs cannot be resolved at the moment, even though it would be technically possible. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (2)
mlir/include/mlir/Dialect/QC/IR/QCOps.td (1)
1016-1022:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winClarify negative fractional-power semantics in the op description.
Line 1022 still documents
r < 0asinv @ pow(-r) @ gunconditionally, but that rewrite is only sound for integer exponents. For fractional negative powers, the canonical form must stay aspow(-r){U}because principal-branch semantics differ on unitaries with eigenvalue-1(e.g.H,X,Y,Z,ECR,SWAP).Based on learnings,
NegPowToInvPowis only mathematically correct for integer exponents and negative fractional exponents must not be documented as equivalent topow(-r){inv{U}}.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/include/mlir/Dialect/QC/IR/QCOps.td` around lines 1016 - 1022, Update the QCOps.td description for the modifier operation to clarify negative-power semantics: state that the rewrite "r < 0: inv @ pow(-r) @ g" is only valid for integer exponents and must not be applied to fractional negative exponents; for negative fractional r the canonical form remains pow(-r){U} because principal-branch semantics can differ for unitaries with eigenvalue -1 (e.g. H, X, Y, Z, ECR, SWAP); also reference the transformation rule name NegPowToInvPow to indicate it is restricted to integer exponents.mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)
166-203:⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy liftClone the full body when rebuilding nested modifiers.
NegPowToInvPow,MergeNestedPow, andMoveCtrlOutsideonly clonegetBodyUnitary(). For cases likepow(-2) { rx(0.123) }, thearith.constantfor0.123lives in the old body, so the cloned gate in the new modifier still references a value from the region being erased. That produces invalid IR and reintroduces the dangling-value failure you already handled inFoldPowIntoGate.Please inline/move the non-
YieldOpbody ops together with the unitary, or clone/remap the unitary’s operand-def chain into the new body before replacingop.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp` around lines 166 - 203, NegPowToInvPow, MergeNestedPow and MoveCtrlOutside currently only clone getBodyUnitary() into the newly created modifier body which leaves upstream ops (e.g. constants) referenced from the old region and causes dangling-value IR; fix each pattern (the matchAndRewrite implementations that call rewriter.replaceOpWithNewOp and PowOp::create / InvOp::create) to also clone or move the non-Yield ops that the unitary depends on into the new region (or clone/remap the unitary’s operand-def chain) before replacing the original op so all SSA operands in the new body point to values inside the new region (similar to the work done in FoldPowIntoGate but applied here to preserve constants and other defs).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp`:
- Around line 489-535: PowOp currently assumes the exponent is a constant in
getExponentValue(), but verify() doesn't enforce that; update PowOp::verify() to
reject dynamic exponents by checking getExponent() is an arith.constant (use
matchPattern(getExponent(), m_Constant(&attr)) or equivalent) and emit an op
error (e.g., "exponent must be a constant") when it is not, so
getExponentValue() is only called on verified ops; reference
PowOp::getExponentValue, PowOp::verify, getExponent(), and
matchPattern/m_Constant.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 624-683: PowOp::verify must reject non-constant exponents to avoid
hard aborts in downstream canonicalizations; update PowOp::verify to check that
the exponent operand (getExponent()) is a constant floating-point value (the
same form expected by getExponentValue()/getUnitaryMatrix()) and emitOpError if
it is not, so IR with non-constant exponents cannot be constructed, or
alternatively adjust getExponentValue() callers to gracefully bail out—prefer
the constants-only fix by adding a const-check in PowOp::verify referencing
getExponent() and getExponentValue() to locate the relevant logic.
In `@mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp`:
- Around line 149-157: Add the missing regression tests that cover fractional
exponents and the control-modifier context: add a QCTestCase that exercises
inv(pow(0.5){H}) and expects pow(-0.5){H} (use the appropriate builders, e.g.,
invPowHFrac and powHFracNeg) and add a QCTestCase that instantiates ctrlPowSx
with its reference (e.g., ctrlPowSx and ctrlPowSxRef) so the ctrl { pow(1/3){sx}
} path stays blocked; ensure these new cases reference the same test harness
symbols as the others and rely on MovePowOutside emitting pow(-p){U} (not
wrapping in inv).
In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp`:
- Around line 255-275: Add the missing regression case for the multi-op
expansion path by adding the ctrlPowSx test into the INSTANTIATE_TEST_SUITE_P
list: create a QCOTestCase entry using MQT_NAMED_BUILDER(ctrlPowSx) and the
appropriate reference builder (e.g., MQT_NAMED_BUILDER(ctrlPowSxRef) or the
existing reference name from qco_programs.cpp) so the INSTANTIATE_TEST_SUITE_P
(QCOPowOpTest, QCOTest, ...) covers the ctrlPowSx scenario that triggers
sx->gphase+rx expansion and validates the PowOp-in-modifier bug.
- Around line 267-268: The test case using QCOTestCase{"NegPowH",
MQT_NAMED_BUILDER(negPowH), MQT_NAMED_BUILDER(negPowH)} incorrectly
self-compares; change it to assert structure instead of using negPowH as the
reference so the rewrite NegPowToInvPow cannot hide an invalid transformation.
Update the "NegPowH" test to run the pipeline on negPowH and then verify the
cleaned IR still contains a pow node with a negative fractional exponent applied
to H (e.g., pow(-0.5){ h } or equivalent AST node), rather than comparing
against the same builder; locate the QCOTestCase entry for "NegPowH" and replace
the reference builder with an assertion helper that inspects the resulting IR
for a pow node with exponent -0.5 and base H (or fails if an inv-based form
appears).
- Around line 263-264: The test compares powRxx to itself so a broken
angle-scaling rewrite would still pass; update the QCOTestCase entry for
"PowRxx" to use a distinct reference builder that emits the already-scaled rxx
(i.e., rxx(2θ)) instead of MQT_NAMED_BUILDER(powRxx) twice. Replace the second
MQT_NAMED_BUILDER(powRxx) with a dedicated builder function name (e.g.,
powRxxScaled or powRxxRef) that constructs the expected canonical form, or
supply an inline/reference builder that produces rxx with the scaled angle;
ensure the unique symbol you add is used in the test entry so the case compares
the rewritten result to the correct scaled reference.
In `@mlir/unittests/programs/qco_programs.h`:
- Around line 1184-1212: Add a regression case that constructs inv(pow(0.5){H})
and asserts it canonicalizes to the same form produced by negPowH rather than to
pow(0.5){inv{H}}; specifically, in the test suite create a new helper/test that
uses QCOProgramBuilder to build inv(pow(0.5){H}), run the
canonicalization/passes that trigger InvPowToNegPow, and compare the result
against negPowH (reuse negPowH as the expected reference); ensure the test
references the inv(pow(...){...}) source form and the negPowH reference so the
new rewrite is exercised and validated.
---
Duplicate comments:
In `@mlir/include/mlir/Dialect/QC/IR/QCOps.td`:
- Around line 1016-1022: Update the QCOps.td description for the modifier
operation to clarify negative-power semantics: state that the rewrite "r < 0:
inv @ pow(-r) @ g" is only valid for integer exponents and must not be applied
to fractional negative exponents; for negative fractional r the canonical form
remains pow(-r){U} because principal-branch semantics can differ for unitaries
with eigenvalue -1 (e.g. H, X, Y, Z, ECR, SWAP); also reference the
transformation rule name NegPowToInvPow to indicate it is restricted to integer
exponents.
In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp`:
- Around line 166-203: NegPowToInvPow, MergeNestedPow and MoveCtrlOutside
currently only clone getBodyUnitary() into the newly created modifier body which
leaves upstream ops (e.g. constants) referenced from the old region and causes
dangling-value IR; fix each pattern (the matchAndRewrite implementations that
call rewriter.replaceOpWithNewOp and PowOp::create / InvOp::create) to also
clone or move the non-Yield ops that the unitary depends on into the new region
(or clone/remap the unitary’s operand-def chain) before replacing the original
op so all SSA operands in the new body point to values inside the new region
(similar to the work done in FoldPowIntoGate but applied here to preserve
constants and other defs).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2ab9a6c3-b681-421d-bb07-b876cdca1610
📒 Files selected for processing (23)
CHANGELOG.mdmlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.hmlir/include/mlir/Dialect/QC/IR/QCOps.tdmlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.hmlir/include/mlir/Dialect/QCO/IR/QCOOps.tdmlir/include/mlir/Dialect/Utils/Utils.hmlir/lib/Conversion/QCOToQC/QCOToQC.cppmlir/lib/Conversion/QCToQCO/QCToQCO.cppmlir/lib/Dialect/QC/Builder/QCProgramBuilder.cppmlir/lib/Dialect/QC/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QC/IR/Modifiers/PowOp.cppmlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cppmlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cppmlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cppmlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cppmlir/unittests/Dialect/QC/IR/test_qc_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cppmlir/unittests/programs/qc_programs.cppmlir/unittests/programs/qc_programs.hmlir/unittests/programs/qco_programs.cppmlir/unittests/programs/qco_programs.h
| INSTANTIATE_TEST_SUITE_P( | ||
| QCOPowOpTest, QCOTest, | ||
| testing::Values(QCOTestCase{"Pow1Inline", MQT_NAMED_BUILDER(pow1Inline), | ||
| MQT_NAMED_BUILDER(rx)}, | ||
| QCOTestCase{"Pow0Erase", MQT_NAMED_BUILDER(pow0Erase), | ||
| MQT_NAMED_BUILDER(emptyQCO)}, | ||
| QCOTestCase{"NestedPow", MQT_NAMED_BUILDER(nestedPow), | ||
| MQT_NAMED_BUILDER(powSingleExponent)}, | ||
| QCOTestCase{"PowRxx", MQT_NAMED_BUILDER(powRxx), | ||
| MQT_NAMED_BUILDER(powRxx)}, | ||
| QCOTestCase{"NegPowRx", MQT_NAMED_BUILDER(negPowRx), | ||
| MQT_NAMED_BUILDER(powRxNeg)}, | ||
| QCOTestCase{"NegPowH", MQT_NAMED_BUILDER(negPowH), | ||
| MQT_NAMED_BUILDER(negPowH)}, | ||
| QCOTestCase{"InvPowRx", MQT_NAMED_BUILDER(invPowRx), | ||
| MQT_NAMED_BUILDER(powRxNeg)}, | ||
| QCOTestCase{"PowCtrlRx", MQT_NAMED_BUILDER(powCtrlRx), | ||
| MQT_NAMED_BUILDER(ctrlPowRx)}, | ||
| QCOTestCase{"NegPowInvIswap", | ||
| MQT_NAMED_BUILDER(negPowInvIswap), | ||
| MQT_NAMED_BUILDER(negPowInvIswapRef)})); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
The suite is still missing the ctrlPowSx regression case.
The hard PowOp-in-modifier bug for this PR only shows up when pow would expand into multiple ops, e.g. sx -> gphase + rx. The current PowCtrlRx / CtrlPowRx entries stay single-op, and ctrlPowSx from qco_programs.cpp is not instantiated here, so that path is still untested.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp` around lines 255 - 275, Add
the missing regression case for the multi-op expansion path by adding the
ctrlPowSx test into the INSTANTIATE_TEST_SUITE_P list: create a QCOTestCase
entry using MQT_NAMED_BUILDER(ctrlPowSx) and the appropriate reference builder
(e.g., MQT_NAMED_BUILDER(ctrlPowSxRef) or the existing reference name from
qco_programs.cpp) so the INSTANTIATE_TEST_SUITE_P (QCOPowOpTest, QCOTest, ...)
covers the ctrlPowSx scenario that triggers sx->gphase+rx expansion and
validates the PowOp-in-modifier bug.
| QCOTestCase{"PowRxx", MQT_NAMED_BUILDER(powRxx), | ||
| MQT_NAMED_BUILDER(powRxx)}, |
There was a problem hiding this comment.
PowRxx currently can't fail for a broken angle-scaling rewrite.
This compares powRxx against itself, so the case still passes if pow(2){rxx(θ)} forgets to canonicalize to rxx(2θ)}. Please use a distinct reference builder that emits the scaled rxx directly.
Suggested wiring
- QCOTestCase{"PowRxx", MQT_NAMED_BUILDER(powRxx),
- MQT_NAMED_BUILDER(powRxx)},
+ QCOTestCase{"PowRxx", MQT_NAMED_BUILDER(powRxx),
+ MQT_NAMED_BUILDER(powRxxRef)},+void powRxxRef(QCOProgramBuilder& b) {
+ auto q = b.allocQubitRegister(2);
+ b.rxx(2.0 * 0.123, q[0], q[1]);
+}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp` around lines 263 - 264, The
test compares powRxx to itself so a broken angle-scaling rewrite would still
pass; update the QCOTestCase entry for "PowRxx" to use a distinct reference
builder that emits the already-scaled rxx (i.e., rxx(2θ)) instead of
MQT_NAMED_BUILDER(powRxx) twice. Replace the second MQT_NAMED_BUILDER(powRxx)
with a dedicated builder function name (e.g., powRxxScaled or powRxxRef) that
constructs the expected canonical form, or supply an inline/reference builder
that produces rxx with the scaled angle; ensure the unique symbol you add is
used in the test entry so the case compares the rewritten result to the correct
scaled reference.
| QCOTestCase{"NegPowH", MQT_NAMED_BUILDER(negPowH), | ||
| MQT_NAMED_BUILDER(negPowH)}, |
There was a problem hiding this comment.
NegPowH needs a structural assertion, not a self-comparison.
This is the exact fractional-negative case that must not go through NegPowToInvPow. Using negPowH as both program and reference runs the same cleanup twice, so an invalid rewrite would still pass. Please make this a dedicated test that asserts the cleaned IR still contains pow(-0.5){ h } rather than an inv-based form.
Based on learnings, NegPowToInvPow is only mathematically sound for integer exponents; negative fractional powers of Hermitian gates like H must remain as pow(-r){U}.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp` around lines 267 - 268, The
test case using QCOTestCase{"NegPowH", MQT_NAMED_BUILDER(negPowH),
MQT_NAMED_BUILDER(negPowH)} incorrectly self-compares; change it to assert
structure instead of using negPowH as the reference so the rewrite
NegPowToInvPow cannot hide an invalid transformation. Update the "NegPowH" test
to run the pipeline on negPowH and then verify the cleaned IR still contains a
pow node with a negative fractional exponent applied to H (e.g., pow(-0.5){ h }
or equivalent AST node), rather than comparing against the same builder; locate
the QCOTestCase entry for "NegPowH" and replace the reference builder with an
assertion helper that inspects the resulting IR for a pow node with exponent
-0.5 and base H (or fails if an inv-based form appears).
| /// Creates a circuit with pow(-0.5) wrapping H (negative non-integer exponent). | ||
| /// Expected to remain unchanged: fractional exponent on a unitary with | ||
| /// eigenvalue -1 cannot safely apply NegPowToInvPow. | ||
| void negPowH(QCOProgramBuilder& b); | ||
|
|
||
| /// Creates a circuit with inv wrapping pow (should reorder to pow wrapping | ||
| /// inv). | ||
| void invPowRx(QCOProgramBuilder& b); | ||
|
|
||
| /// Creates a circuit with pow wrapping ctrl wrapping RX (should move ctrl | ||
| /// outside). | ||
| void powCtrlRx(QCOProgramBuilder& b); | ||
|
|
||
| /// Creates a circuit with ctrl wrapping pow wrapping RX (reference for | ||
| /// powCtrlRx). | ||
| void ctrlPowRx(QCOProgramBuilder& b); | ||
|
|
||
| /// Creates a circuit with pow(-2) wrapping inv wrapping iSWAP. | ||
| /// Exercises NegPowToInvPow: inv{iswap} survives InvOp canonicalization, | ||
| /// FoldPowIntoGate fails (inner is InvOp), so NegPowToInvPow fires. | ||
| void negPowInvIswap(QCOProgramBuilder& b); | ||
|
|
||
| /// Reference for negPowInvIswap: xx_plus_yy(-2π, 0) (the fully folded form). | ||
| void negPowInvIswapRef(QCOProgramBuilder& b); | ||
|
|
||
| /// Creates a circuit with ctrl wrapping pow(1/3) wrapping SX. The fold | ||
| /// pow(p){SX} → gphase+rx is suppressed inside ctrl (would emit two ops), | ||
| /// so the pow survives canonicalization and reaches ConvertQCOPowOp. | ||
| void ctrlPowSx(QCOProgramBuilder& b); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Add a regression case for inv(pow(0.5){H}).
This header already adds negPowH, but the new InvPowToNegPow rewrite is only really pinned if you also exercise the source form that triggers it: inv(pow(0.5){H}) should canonicalize to that same negative-fractional pow form, not to pow(0.5){inv{H}}. Reusing negPowH as the reference would make this a very small but high-value coverage addition.
Based on learnings: inv(pow(p){U}) must be rewritten by negating the exponent, and negative fractional powers on Hermitian/self-inverse gates must remain as pow(-r){U} because the inverse-inside-power form is not mathematically sound.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mlir/unittests/programs/qco_programs.h` around lines 1184 - 1212, Add a
regression case that constructs inv(pow(0.5){H}) and asserts it canonicalizes to
the same form produced by negPowH rather than to pow(0.5){inv{H}}; specifically,
in the test suite create a new helper/test that uses QCOProgramBuilder to build
inv(pow(0.5){H}), run the canonicalization/passes that trigger InvPowToNegPow,
and compare the result against negPowH (reuse negPowH as the expected
reference); ensure the test references the inv(pow(...){...}) source form and
the negPowH reference so the new rewrite is exercised and validated.
## Description This is a rebased version of #1671 after #1671 was automatically closed when #1751 was merged. Original description by @J4MMlE: > ## Description > > This PR adds a direct OpenQASM-to-QC translation that bypasses the `qc::QuantumComputation`. > > The new flow reuses the existing parser and walks the AST directly to emit QC dialect ops. Skipping `QuantumComputation` removes an intermediate step and enables previously unsupported QASM3 features, such as gate modifiers, classical computations, and control flow structures. > > ### What works > > - All standard gates plus Qiskit-style MCX variants > - Gate modifiers: `ctrl @`, `negctrl @`, `inv @`, nested combinations > - Register declarations, qubit/bit allocation > - Measure, reset, barrier > - `if`/`else` over quantum statements > - Hardware qubits convert to `qc.static` > - Broadcasting (register-width gate calls) > > ### Current limitations > > - Gate parameters must be compile-time constants > - `pow` modifier is unsupported (but will/should be once [#1603](#1603) is merged) > - Layout pragmas are unsupported (they have no equivalent in `QC`) ## Checklist - [x] The pull request only contains commits that are focused and relevant to this change. - [x] I have added appropriate tests that cover the new/changed functionality. - [x] ~~I have updated the documentation to reflect these changes.~~ - [x] I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals. - [x] ~~I have added migration instructions to the upgrade guide (if needed).~~ - [x] The changes follow the project's style guidelines and introduce no new warnings. - [x] The changes are fully tested and pass the CI checks. - [x] I have reviewed my own code changes. --------- Signed-off-by: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Co-authored-by: Anatol Bussejahn <anatol.bussejahn@tum.de> Co-authored-by: Lukas Burgholzer <burgholzer@me.com>
|
@coderabbitai continue |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp (2)
694-724: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMake the matrix-availability predicate match
getUnitaryMatrix().Line 695 returns true for
pow(2.0) { x }becausexhas a known matrix, but lines 723-724 still returnstd::nulloptfor every non-0/1 exponent. GatehasCompileTimeKnownUnitaryMatrix()on the exponent cases actually implemented, or implement the general power computation.🛠️ Minimal predicate fix
bool PowOp::hasCompileTimeKnownUnitaryMatrix() { - return all_of(getBody()->getOps<UnitaryOpInterface>(), - [](UnitaryOpInterface op) { - return op.hasCompileTimeKnownUnitaryMatrix(); - }); + const double p = getExponentValue(); + if (std::abs(p) >= TOLERANCE && std::abs(p - 1.0) >= TOLERANCE) { + return false; + } + auto bodyUnitary = utils::getSoleBodyUnitary<UnitaryOpInterface>(*getBody()); + return bodyUnitary && bodyUnitary.hasCompileTimeKnownUnitaryMatrix(); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 694 - 724, The hasCompileTimeKnownUnitaryMatrix() method does not check the exponent value, causing it to return true even when getUnitaryMatrix() returns nullopt for unsupported exponents. Fix this inconsistency by adding exponent validation to hasCompileTimeKnownUnitaryMatrix(): use getExponentValue() to retrieve the exponent and check that it matches one of the supported cases (approximately equal to 0 or 1 using TOLERANCE comparisons), ensuring the predicate only returns true when getUnitaryMatrix() can actually produce a result.
621-686: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReject non-unitary quantum side effects in
PowOp::verify.Line 657 only rejects extra unitary ops, so a
powbody can still containalloc/measure/reset/qtensor mutation before the sole unitary and be treated as a valid power modifier. Mirror theInvOpguard here, and reject duplicate input qubits while tightening the verifier.🛡️ Suggested verifier tightening
LogicalResult PowOp::verify() { FloatAttr attr; if (!matchPattern(getExponent(), m_Constant(&attr))) { return emitOpError("exponent must be a constant"); } auto& block = *getBody(); + if (llvm::any_of(block, [](Operation& nested) { + return isa<AllocOp, SinkOp, MeasureOp, ResetOp, qtensor::ExtractOp, + qtensor::InsertOp>(nested); + })) { + return emitOpError("body must not contain non-unitary quantum operations " + "or modify a quantum register"); + } + if (block.getOperations().size() < 2) { return emitOpError("body region must have at least two operations"); } const auto numTargets = getNumTargets(); + SmallPtrSet<Value, 4> uniqueQubitsIn; + for (Value target : getQubitsIn()) { + if (!uniqueQubitsIn.insert(target).second) { + return emitOpError("duplicate qubit found"); + } + } + if (block.getArguments().size() != numTargets) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 621 - 686, The PowOp::verify() method currently only rejects duplicate unitary operations but allows non-unitary quantum side effects like alloc, measure, reset, and qtensor mutations to exist in the body region before the unitary. Strengthen the verification loop that starts with the ++iter iterator (which currently checks for extra UnitaryOpInterface operations) to also reject any non-unitary quantum side effects in the body, mirroring the approach used in InvOp. Additionally, add validation to reject duplicate input qubits by ensuring each block argument used by the body unitary is unique and matches the corresponding target argument without duplication.mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp (1)
101-112: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
InvPowToNegPowcan drop required preamble defs and create dangling uses.Line 108 inlines only
innerPow’s body. If that body uses values defined earlier ininvOp’s body, those producer ops are erased withinvOp, leaving invalid IR.🔧 Proposed fix
const double exponent = innerPow.getExponentValue(); // Remap InvOp's block arguments to the outer qubits they alias, // since the inlined ops may reference them and InvOp is about to be erased. for (auto [blockArg, outerQubit] : llvm::zip_equal(invOp.getBody()->getArguments(), invOp.getQubits())) { rewriter.replaceAllUsesWith(blockArg, outerQubit); } + + // Preserve non-unitary preamble ops that may define values captured by the + // nested pow body. + for (auto& bodyOp : llvm::make_early_inc_range(*invOp.getBody())) { + if (&bodyOp != innerPow.getOperation() && !isa<YieldOp>(bodyOp)) { + rewriter.moveOpBefore(&bodyOp, invOp); + } + } rewriter.replaceOpWithNewOp<PowOp>(invOp, -exponent, [&] { auto* powBody = rewriter.getInsertionBlock(); rewriter.inlineBlockBefore(innerPow.getBody(), powBody, powBody->begin()); rewriter.eraseOp(&powBody->back()); // erase the inlined YieldOp });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp` around lines 101 - 112, The issue is that when inlining only innerPow.getBody() into the new PowOp, any preamble operations that define values used by innerPow's body will be lost when invOp is erased. Fix this by identifying and inlining any preamble definitions from invOp's body (operations that come before the innerPow operation and produce values used by innerPow) into the new PowOp before inlining innerPow.getBody(). This ensures that dependent definitions are preserved and not dangling references are created.mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)
555-580: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winTighten
PowOp::verifyto reject non-unitary quantum ops in the body.Line 557 validates constant exponent, but the verifier still allows side-effecting non-unitary quantum ops in the preamble. That violates modifier legality and can make canonicalizations unsafe when they restructure body contents.
🔧 Proposed fix
LogicalResult PowOp::verify() { FloatAttr attr; if (!matchPattern(getExponent(), m_Constant(&attr))) { return emitOpError("exponent must be a constant"); } auto& block = *getBody(); + if (llvm::any_of(block, [](Operation& op) { + return isa<AllocOp, DeallocOp, MeasureOp, ResetOp>(op); + })) { + return emitOpError("body must not contain non-unitary quantum operations"); + } + if (block.getOperations().size() < 2) { return emitOpError("body region must have at least two operations"); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp` around lines 555 - 580, The verification logic in PowOp::verify currently only rejects unitary operations in the preamble (the loop starting with `for (auto it = ++iter; it != block.rend(); ++it)`), but it does not reject non-unitary quantum operations that would violate modifier legality. Enhance the loop that iterates through preamble operations to add an additional check that rejects non-unitary quantum ops alongside the existing rejection of unitary ops, ensuring only operations that are neither unitary quantum ops nor side-effecting quantum ops are permitted in the body before the required unitary operation.mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)
725-748: 🩺 Stability & Availability | 🔴 CriticalUse
moveRegionwith type converter for block argument conversion inqco.powpattern.
cloneRegionBeforepreserves theqco.powentry block argument types (!qco.qubit), but line 742 replaces them withadaptor.getQubitsIn()which contains!qc.qubitvalues (post-type-conversion from the OpAdaptor). This creates a type mismatch. TheConvertQCOCtrlOpandConvertQCOInvOppatterns already usemoveRegionwithgetTypeConverter()to properly convert block argument types before RAUW. Apply the same approach here to ensure type safety.Proposed fix
- // Clone body region from QCO to QC - auto& dstRegion = qcOp.getRegion(); - rewriter.cloneRegionBefore(op.getRegion(), dstRegion, dstRegion.end()); - - auto& entryBlock = dstRegion.front(); - const auto numArgs = entryBlock.getNumArguments(); + auto& srcEntryBlock = op.getRegion().front(); + const auto numArgs = srcEntryBlock.getNumArguments(); if (adaptor.getQubitsIn().size() != numArgs) { return op.emitOpError() << "qco.pow: entry block args (" << numArgs << ") must match number of target operands (" << adaptor.getQubitsIn().size() << ")"; } + + // Move body region from QCO to QC and convert block args before RAUW. + auto& dstRegion = qcOp.getRegion(); + if (failed(moveRegion(op.getRegion(), dstRegion, rewriter, + getTypeConverter()))) { + return failure(); + } + auto& entryBlock = dstRegion.front(); // Remove all block arguments in the cloned region rewriter.modifyOpInPlace(qcOp, [&] {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp` around lines 725 - 748, The `cloneRegionBefore` call in the qco.pow pattern conversion preserves the original `!qco.qubit` block argument types, but then replaces them with `!qc.qubit` values from the adaptor, causing a type mismatch. Instead of using `cloneRegionBefore` followed by manual argument replacement, use `moveRegion` with `getTypeConverter()` to properly convert the block argument types during the region move, similar to how `ConvertQCOCtrlOp` and `ConvertQCOInvOp` patterns handle this conversion. This will ensure the block arguments are converted from `!qco.qubit` to `!qc.qubit` types automatically and safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h`:
- Around line 552-553: The example documentation for the `qc.ctrl` operation in
the QCProgramBuilder.h file is missing the `targets` keyword in the syntax.
Update the example that currently shows `qc.ctrl(%q0) (%a0 = %q1)` to include
the `targets` keyword so it reads `qc.ctrl(%q0) targets(%a0 = %q1)` to align
with the current API documentation and prevent confusion for users copying the
example.
In `@mlir/include/mlir/Dialect/Utils/Utils.h`:
- Around line 251-257: The function getBodyUnitary calls std::next to advance an
iterator by i positions before checking bounds, which results in undefined
behavior since std::next performs no bounds validation. To fix this, manually
iterate through the filtered unitaries range while counting elements, checking
if the count reaches i and if iteration completes without reaching the target
index. Return the unitary operation when the count matches i, or call
llvm::reportFatalUsageError if iteration completes without finding the i-th
unitary.
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 127-138: The inlineRegion function currently inlines regions and
then attempts to replace block arguments with type-converted adaptor values,
violating MLIR's same-type RAUW invariant when the adaptor values have different
types than the original block arguments. Modify inlineRegion to use the type
converter to convert the block argument types before performing the
replaceAllUsesWith operation, following the pattern used by moveRegion from
ConversionUtils.h. Change the function signature to return LogicalResult instead
of void to properly communicate conversion failures. Update all call sites of
inlineRegion (in the ForOp pattern around line 822, WhileOp pattern around lines
868-870, and IfOp pattern around lines 913-918) to handle the returned
LogicalResult.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp`:
- Around line 403-430: The InvOp::verify() method dereferences
block.getTerminator() without first validating that the body is non-empty and
that the terminator is specifically a YieldOp. Before line 426 where
getTerminator() is called, add a verification check to ensure the terminator
exists and is a YieldOp instance, returning an appropriate emitOpError if either
condition fails. This will prevent null dereferences and reject malformed IR
that contains non-yield terminators.
- Around line 343-355: The EraseEmptyInv struct's matchAndRewrite method removes
InvOp operations without checking if they are nested as the sole body unitary
within another modifier, which violates modifier body constraints. Before the
current check on op.getNumBodyUnitaries() in matchAndRewrite, add logic to
detect if the InvOp is nested inside a parent modifier body and handle it
accordingly: for single-target nested cases, replace with an IdOp instead of
removing it; for multi-target nested cases, return failure to prevent the
rewrite. This approach should mirror the handling used in the pow(0) pattern to
maintain parent modifier validity.
- Around line 448-517: The hasCompileTimeKnownUnitaryMatrix() function returns
true for cases where getUnitaryMatrix() returns std::nullopt, causing
inconsistency. Modify the hasCompileTimeKnownUnitaryMatrix() predicate to
include the same conditional checks that getUnitaryMatrix() implements: it
should verify that getNumBodyUnitaries() returns zero, or that there is a sole
body unitary available, or that getNumTargets() equals one (for the composition
case). This ensures the predicate only returns true when getUnitaryMatrix() will
successfully return a matrix rather than std::nullopt.
---
Outside diff comments:
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 725-748: The `cloneRegionBefore` call in the qco.pow pattern
conversion preserves the original `!qco.qubit` block argument types, but then
replaces them with `!qc.qubit` values from the adaptor, causing a type mismatch.
Instead of using `cloneRegionBefore` followed by manual argument replacement,
use `moveRegion` with `getTypeConverter()` to properly convert the block
argument types during the region move, similar to how `ConvertQCOCtrlOp` and
`ConvertQCOInvOp` patterns handle this conversion. This will ensure the block
arguments are converted from `!qco.qubit` to `!qc.qubit` types automatically and
safely.
In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp`:
- Around line 101-112: The issue is that when inlining only innerPow.getBody()
into the new PowOp, any preamble operations that define values used by
innerPow's body will be lost when invOp is erased. Fix this by identifying and
inlining any preamble definitions from invOp's body (operations that come before
the innerPow operation and produce values used by innerPow) into the new PowOp
before inlining innerPow.getBody(). This ensures that dependent definitions are
preserved and not dangling references are created.
In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp`:
- Around line 555-580: The verification logic in PowOp::verify currently only
rejects unitary operations in the preamble (the loop starting with `for (auto it
= ++iter; it != block.rend(); ++it)`), but it does not reject non-unitary
quantum operations that would violate modifier legality. Enhance the loop that
iterates through preamble operations to add an additional check that rejects
non-unitary quantum ops alongside the existing rejection of unitary ops,
ensuring only operations that are neither unitary quantum ops nor side-effecting
quantum ops are permitted in the body before the required unitary operation.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 694-724: The hasCompileTimeKnownUnitaryMatrix() method does not
check the exponent value, causing it to return true even when getUnitaryMatrix()
returns nullopt for unsupported exponents. Fix this inconsistency by adding
exponent validation to hasCompileTimeKnownUnitaryMatrix(): use
getExponentValue() to retrieve the exponent and check that it matches one of the
supported cases (approximately equal to 0 or 1 using TOLERANCE comparisons),
ensuring the predicate only returns true when getUnitaryMatrix() can actually
produce a result.
- Around line 621-686: The PowOp::verify() method currently only rejects
duplicate unitary operations but allows non-unitary quantum side effects like
alloc, measure, reset, and qtensor mutations to exist in the body region before
the unitary. Strengthen the verification loop that starts with the ++iter
iterator (which currently checks for extra UnitaryOpInterface operations) to
also reject any non-unitary quantum side effects in the body, mirroring the
approach used in InvOp. Additionally, add validation to reject duplicate input
qubits by ensuring each block argument used by the body unitary is unique and
matches the corresponding target argument without duplication.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 10c356a3-8858-4c5a-8f0f-1d7dd0b4f96c
📒 Files selected for processing (23)
CHANGELOG.mdmlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.hmlir/include/mlir/Dialect/QC/IR/QCOps.tdmlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.hmlir/include/mlir/Dialect/QCO/IR/QCOOps.tdmlir/include/mlir/Dialect/Utils/Utils.hmlir/lib/Conversion/QCOToQC/QCOToQC.cppmlir/lib/Conversion/QCToQCO/QCToQCO.cppmlir/lib/Dialect/QC/Builder/QCProgramBuilder.cppmlir/lib/Dialect/QC/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QC/IR/Modifiers/PowOp.cppmlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cppmlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cppmlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cppmlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cppmlir/unittests/Dialect/QC/IR/test_qc_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cppmlir/unittests/programs/qc_programs.cppmlir/unittests/programs/qc_programs.hmlir/unittests/programs/qco_programs.cppmlir/unittests/programs/qco_programs.h
💤 Files with no reviewable changes (9)
- mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
- mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
- mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
- mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
- mlir/unittests/programs/qc_programs.h
- mlir/unittests/programs/qco_programs.h
- mlir/unittests/programs/qco_programs.cpp
- mlir/unittests/programs/qc_programs.cpp
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp (2)
694-724: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMake the matrix-availability predicate match
getUnitaryMatrix().Line 695 returns true for
pow(2.0) { x }becausexhas a known matrix, but lines 723-724 still returnstd::nulloptfor every non-0/1 exponent. GatehasCompileTimeKnownUnitaryMatrix()on the exponent cases actually implemented, or implement the general power computation.🛠️ Minimal predicate fix
bool PowOp::hasCompileTimeKnownUnitaryMatrix() { - return all_of(getBody()->getOps<UnitaryOpInterface>(), - [](UnitaryOpInterface op) { - return op.hasCompileTimeKnownUnitaryMatrix(); - }); + const double p = getExponentValue(); + if (std::abs(p) >= TOLERANCE && std::abs(p - 1.0) >= TOLERANCE) { + return false; + } + auto bodyUnitary = utils::getSoleBodyUnitary<UnitaryOpInterface>(*getBody()); + return bodyUnitary && bodyUnitary.hasCompileTimeKnownUnitaryMatrix(); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 694 - 724, The hasCompileTimeKnownUnitaryMatrix() method does not check the exponent value, causing it to return true even when getUnitaryMatrix() returns nullopt for unsupported exponents. Fix this inconsistency by adding exponent validation to hasCompileTimeKnownUnitaryMatrix(): use getExponentValue() to retrieve the exponent and check that it matches one of the supported cases (approximately equal to 0 or 1 using TOLERANCE comparisons), ensuring the predicate only returns true when getUnitaryMatrix() can actually produce a result.
621-686: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReject non-unitary quantum side effects in
PowOp::verify.Line 657 only rejects extra unitary ops, so a
powbody can still containalloc/measure/reset/qtensor mutation before the sole unitary and be treated as a valid power modifier. Mirror theInvOpguard here, and reject duplicate input qubits while tightening the verifier.🛡️ Suggested verifier tightening
LogicalResult PowOp::verify() { FloatAttr attr; if (!matchPattern(getExponent(), m_Constant(&attr))) { return emitOpError("exponent must be a constant"); } auto& block = *getBody(); + if (llvm::any_of(block, [](Operation& nested) { + return isa<AllocOp, SinkOp, MeasureOp, ResetOp, qtensor::ExtractOp, + qtensor::InsertOp>(nested); + })) { + return emitOpError("body must not contain non-unitary quantum operations " + "or modify a quantum register"); + } + if (block.getOperations().size() < 2) { return emitOpError("body region must have at least two operations"); } const auto numTargets = getNumTargets(); + SmallPtrSet<Value, 4> uniqueQubitsIn; + for (Value target : getQubitsIn()) { + if (!uniqueQubitsIn.insert(target).second) { + return emitOpError("duplicate qubit found"); + } + } + if (block.getArguments().size() != numTargets) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 621 - 686, The PowOp::verify() method currently only rejects duplicate unitary operations but allows non-unitary quantum side effects like alloc, measure, reset, and qtensor mutations to exist in the body region before the unitary. Strengthen the verification loop that starts with the ++iter iterator (which currently checks for extra UnitaryOpInterface operations) to also reject any non-unitary quantum side effects in the body, mirroring the approach used in InvOp. Additionally, add validation to reject duplicate input qubits by ensuring each block argument used by the body unitary is unique and matches the corresponding target argument without duplication.mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp (1)
101-112: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
InvPowToNegPowcan drop required preamble defs and create dangling uses.Line 108 inlines only
innerPow’s body. If that body uses values defined earlier ininvOp’s body, those producer ops are erased withinvOp, leaving invalid IR.🔧 Proposed fix
const double exponent = innerPow.getExponentValue(); // Remap InvOp's block arguments to the outer qubits they alias, // since the inlined ops may reference them and InvOp is about to be erased. for (auto [blockArg, outerQubit] : llvm::zip_equal(invOp.getBody()->getArguments(), invOp.getQubits())) { rewriter.replaceAllUsesWith(blockArg, outerQubit); } + + // Preserve non-unitary preamble ops that may define values captured by the + // nested pow body. + for (auto& bodyOp : llvm::make_early_inc_range(*invOp.getBody())) { + if (&bodyOp != innerPow.getOperation() && !isa<YieldOp>(bodyOp)) { + rewriter.moveOpBefore(&bodyOp, invOp); + } + } rewriter.replaceOpWithNewOp<PowOp>(invOp, -exponent, [&] { auto* powBody = rewriter.getInsertionBlock(); rewriter.inlineBlockBefore(innerPow.getBody(), powBody, powBody->begin()); rewriter.eraseOp(&powBody->back()); // erase the inlined YieldOp });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp` around lines 101 - 112, The issue is that when inlining only innerPow.getBody() into the new PowOp, any preamble operations that define values used by innerPow's body will be lost when invOp is erased. Fix this by identifying and inlining any preamble definitions from invOp's body (operations that come before the innerPow operation and produce values used by innerPow) into the new PowOp before inlining innerPow.getBody(). This ensures that dependent definitions are preserved and not dangling references are created.mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)
555-580: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winTighten
PowOp::verifyto reject non-unitary quantum ops in the body.Line 557 validates constant exponent, but the verifier still allows side-effecting non-unitary quantum ops in the preamble. That violates modifier legality and can make canonicalizations unsafe when they restructure body contents.
🔧 Proposed fix
LogicalResult PowOp::verify() { FloatAttr attr; if (!matchPattern(getExponent(), m_Constant(&attr))) { return emitOpError("exponent must be a constant"); } auto& block = *getBody(); + if (llvm::any_of(block, [](Operation& op) { + return isa<AllocOp, DeallocOp, MeasureOp, ResetOp>(op); + })) { + return emitOpError("body must not contain non-unitary quantum operations"); + } + if (block.getOperations().size() < 2) { return emitOpError("body region must have at least two operations"); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp` around lines 555 - 580, The verification logic in PowOp::verify currently only rejects unitary operations in the preamble (the loop starting with `for (auto it = ++iter; it != block.rend(); ++it)`), but it does not reject non-unitary quantum operations that would violate modifier legality. Enhance the loop that iterates through preamble operations to add an additional check that rejects non-unitary quantum ops alongside the existing rejection of unitary ops, ensuring only operations that are neither unitary quantum ops nor side-effecting quantum ops are permitted in the body before the required unitary operation.mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)
725-748: 🩺 Stability & Availability | 🔴 CriticalUse
moveRegionwith type converter for block argument conversion inqco.powpattern.
cloneRegionBeforepreserves theqco.powentry block argument types (!qco.qubit), but line 742 replaces them withadaptor.getQubitsIn()which contains!qc.qubitvalues (post-type-conversion from the OpAdaptor). This creates a type mismatch. TheConvertQCOCtrlOpandConvertQCOInvOppatterns already usemoveRegionwithgetTypeConverter()to properly convert block argument types before RAUW. Apply the same approach here to ensure type safety.Proposed fix
- // Clone body region from QCO to QC - auto& dstRegion = qcOp.getRegion(); - rewriter.cloneRegionBefore(op.getRegion(), dstRegion, dstRegion.end()); - - auto& entryBlock = dstRegion.front(); - const auto numArgs = entryBlock.getNumArguments(); + auto& srcEntryBlock = op.getRegion().front(); + const auto numArgs = srcEntryBlock.getNumArguments(); if (adaptor.getQubitsIn().size() != numArgs) { return op.emitOpError() << "qco.pow: entry block args (" << numArgs << ") must match number of target operands (" << adaptor.getQubitsIn().size() << ")"; } + + // Move body region from QCO to QC and convert block args before RAUW. + auto& dstRegion = qcOp.getRegion(); + if (failed(moveRegion(op.getRegion(), dstRegion, rewriter, + getTypeConverter()))) { + return failure(); + } + auto& entryBlock = dstRegion.front(); // Remove all block arguments in the cloned region rewriter.modifyOpInPlace(qcOp, [&] {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp` around lines 725 - 748, The `cloneRegionBefore` call in the qco.pow pattern conversion preserves the original `!qco.qubit` block argument types, but then replaces them with `!qc.qubit` values from the adaptor, causing a type mismatch. Instead of using `cloneRegionBefore` followed by manual argument replacement, use `moveRegion` with `getTypeConverter()` to properly convert the block argument types during the region move, similar to how `ConvertQCOCtrlOp` and `ConvertQCOInvOp` patterns handle this conversion. This will ensure the block arguments are converted from `!qco.qubit` to `!qc.qubit` types automatically and safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h`:
- Around line 552-553: The example documentation for the `qc.ctrl` operation in
the QCProgramBuilder.h file is missing the `targets` keyword in the syntax.
Update the example that currently shows `qc.ctrl(%q0) (%a0 = %q1)` to include
the `targets` keyword so it reads `qc.ctrl(%q0) targets(%a0 = %q1)` to align
with the current API documentation and prevent confusion for users copying the
example.
In `@mlir/include/mlir/Dialect/Utils/Utils.h`:
- Around line 251-257: The function getBodyUnitary calls std::next to advance an
iterator by i positions before checking bounds, which results in undefined
behavior since std::next performs no bounds validation. To fix this, manually
iterate through the filtered unitaries range while counting elements, checking
if the count reaches i and if iteration completes without reaching the target
index. Return the unitary operation when the count matches i, or call
llvm::reportFatalUsageError if iteration completes without finding the i-th
unitary.
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 127-138: The inlineRegion function currently inlines regions and
then attempts to replace block arguments with type-converted adaptor values,
violating MLIR's same-type RAUW invariant when the adaptor values have different
types than the original block arguments. Modify inlineRegion to use the type
converter to convert the block argument types before performing the
replaceAllUsesWith operation, following the pattern used by moveRegion from
ConversionUtils.h. Change the function signature to return LogicalResult instead
of void to properly communicate conversion failures. Update all call sites of
inlineRegion (in the ForOp pattern around line 822, WhileOp pattern around lines
868-870, and IfOp pattern around lines 913-918) to handle the returned
LogicalResult.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp`:
- Around line 403-430: The InvOp::verify() method dereferences
block.getTerminator() without first validating that the body is non-empty and
that the terminator is specifically a YieldOp. Before line 426 where
getTerminator() is called, add a verification check to ensure the terminator
exists and is a YieldOp instance, returning an appropriate emitOpError if either
condition fails. This will prevent null dereferences and reject malformed IR
that contains non-yield terminators.
- Around line 343-355: The EraseEmptyInv struct's matchAndRewrite method removes
InvOp operations without checking if they are nested as the sole body unitary
within another modifier, which violates modifier body constraints. Before the
current check on op.getNumBodyUnitaries() in matchAndRewrite, add logic to
detect if the InvOp is nested inside a parent modifier body and handle it
accordingly: for single-target nested cases, replace with an IdOp instead of
removing it; for multi-target nested cases, return failure to prevent the
rewrite. This approach should mirror the handling used in the pow(0) pattern to
maintain parent modifier validity.
- Around line 448-517: The hasCompileTimeKnownUnitaryMatrix() function returns
true for cases where getUnitaryMatrix() returns std::nullopt, causing
inconsistency. Modify the hasCompileTimeKnownUnitaryMatrix() predicate to
include the same conditional checks that getUnitaryMatrix() implements: it
should verify that getNumBodyUnitaries() returns zero, or that there is a sole
body unitary available, or that getNumTargets() equals one (for the composition
case). This ensures the predicate only returns true when getUnitaryMatrix() will
successfully return a matrix rather than std::nullopt.
---
Outside diff comments:
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 725-748: The `cloneRegionBefore` call in the qco.pow pattern
conversion preserves the original `!qco.qubit` block argument types, but then
replaces them with `!qc.qubit` values from the adaptor, causing a type mismatch.
Instead of using `cloneRegionBefore` followed by manual argument replacement,
use `moveRegion` with `getTypeConverter()` to properly convert the block
argument types during the region move, similar to how `ConvertQCOCtrlOp` and
`ConvertQCOInvOp` patterns handle this conversion. This will ensure the block
arguments are converted from `!qco.qubit` to `!qc.qubit` types automatically and
safely.
In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp`:
- Around line 101-112: The issue is that when inlining only innerPow.getBody()
into the new PowOp, any preamble operations that define values used by
innerPow's body will be lost when invOp is erased. Fix this by identifying and
inlining any preamble definitions from invOp's body (operations that come before
the innerPow operation and produce values used by innerPow) into the new PowOp
before inlining innerPow.getBody(). This ensures that dependent definitions are
preserved and not dangling references are created.
In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp`:
- Around line 555-580: The verification logic in PowOp::verify currently only
rejects unitary operations in the preamble (the loop starting with `for (auto it
= ++iter; it != block.rend(); ++it)`), but it does not reject non-unitary
quantum operations that would violate modifier legality. Enhance the loop that
iterates through preamble operations to add an additional check that rejects
non-unitary quantum ops alongside the existing rejection of unitary ops,
ensuring only operations that are neither unitary quantum ops nor side-effecting
quantum ops are permitted in the body before the required unitary operation.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 694-724: The hasCompileTimeKnownUnitaryMatrix() method does not
check the exponent value, causing it to return true even when getUnitaryMatrix()
returns nullopt for unsupported exponents. Fix this inconsistency by adding
exponent validation to hasCompileTimeKnownUnitaryMatrix(): use
getExponentValue() to retrieve the exponent and check that it matches one of the
supported cases (approximately equal to 0 or 1 using TOLERANCE comparisons),
ensuring the predicate only returns true when getUnitaryMatrix() can actually
produce a result.
- Around line 621-686: The PowOp::verify() method currently only rejects
duplicate unitary operations but allows non-unitary quantum side effects like
alloc, measure, reset, and qtensor mutations to exist in the body region before
the unitary. Strengthen the verification loop that starts with the ++iter
iterator (which currently checks for extra UnitaryOpInterface operations) to
also reject any non-unitary quantum side effects in the body, mirroring the
approach used in InvOp. Additionally, add validation to reject duplicate input
qubits by ensuring each block argument used by the body unitary is unique and
matches the corresponding target argument without duplication.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 10c356a3-8858-4c5a-8f0f-1d7dd0b4f96c
📒 Files selected for processing (23)
CHANGELOG.mdmlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.hmlir/include/mlir/Dialect/QC/IR/QCOps.tdmlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.hmlir/include/mlir/Dialect/QCO/IR/QCOOps.tdmlir/include/mlir/Dialect/Utils/Utils.hmlir/lib/Conversion/QCOToQC/QCOToQC.cppmlir/lib/Conversion/QCToQCO/QCToQCO.cppmlir/lib/Dialect/QC/Builder/QCProgramBuilder.cppmlir/lib/Dialect/QC/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QC/IR/Modifiers/PowOp.cppmlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cppmlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cppmlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cppmlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cppmlir/unittests/Dialect/QC/IR/test_qc_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cppmlir/unittests/programs/qc_programs.cppmlir/unittests/programs/qc_programs.hmlir/unittests/programs/qco_programs.cppmlir/unittests/programs/qco_programs.h
💤 Files with no reviewable changes (9)
- mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
- mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
- mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
- mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
- mlir/unittests/programs/qc_programs.h
- mlir/unittests/programs/qco_programs.h
- mlir/unittests/programs/qco_programs.cpp
- mlir/unittests/programs/qc_programs.cpp
🛑 Comments failed to post (6)
mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h (1)
552-553: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix inconsistent
ctrlMLIR example syntax.This example is missing the
targetskeyword (qc.ctrl(%q0) (%a0 = %q1)), while the updated API/docs elsewhere usetargets(...). Please align it to prevent copy/paste confusion.Suggested doc fix
- * qc.ctrl(%q0) (%a0 = %q1) { \ + * qc.ctrl(%q0) targets(%a0 = %q1) { \📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.* qc.ctrl(%q0) targets(%a0 = %q1) { \ * qc.OP_NAME(%PARAM1, %PARAM2) %a0 : !qc.qubit \🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h` around lines 552 - 553, The example documentation for the `qc.ctrl` operation in the QCProgramBuilder.h file is missing the `targets` keyword in the syntax. Update the example that currently shows `qc.ctrl(%q0) (%a0 = %q1)` to include the `targets` keyword so it reads `qc.ctrl(%q0) targets(%a0 = %q1)` to align with the current API documentation and prevent confusion for users copying the example.mlir/include/mlir/Dialect/Utils/Utils.h (1)
251-257: 🩺 Stability & Availability | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail FILE="$(fd -p 'Utils.h' mlir/include | head -n1)" echo "Inspecting: $FILE" nl -ba "$FILE" | sed -n '245,262p' echo echo "Searching for similar unchecked std::next patterns:" rg -n --type=cpp 'std::next\([^,]+,\s*static_cast<std::ptrdiff_t>\(i\)\)' mlir/include/mlir/DialectRepository: munich-quantum-toolkit/core
Length of output: 264
🏁 Script executed:
#!/bin/bash set -euo pipefail # Find the correct file echo "=== Finding Utils.h in mlir/include/mlir/Dialect/Utils ===" find mlir/include/mlir/Dialect/Utils -name "Utils.h" 2>/dev/null || echo "File not found with find" echo echo "=== Listing files in mlir/include/mlir/Dialect/Utils ===" ls -la mlir/include/mlir/Dialect/Utils/ 2>/dev/null || echo "Directory not found" echo echo "=== Using git ls-files to find Utils.h ===" git ls-files | grep -E "mlir/include.*Utils\.h$" | head -20Repository: munich-quantum-toolkit/core
Length of output: 762
🏁 Script executed:
#!/bin/bash set -euo pipefail # Once we find the correct file, read lines 245-262 FILE="mlir/include/mlir/Dialect/Utils/Utils.h" if [ -f "$FILE" ]; then echo "=== Content of $FILE (lines 245-262) ===" cat -n "$FILE" | sed -n '245,262p' echo echo "=== Full context around getBodyUnitary function ===" cat -n "$FILE" | sed -n '240,270p' else echo "File $FILE not found" fiRepository: munich-quantum-toolkit/core
Length of output: 2538
🏁 Script executed:
#!/bin/bash set -euo pipefail # Search for std::next usage with iterator patterns echo "=== Searching for std::next patterns in MLIR ===" rg -n 'std::next' mlir/include/mlir/Dialect/Utils/ --type=cpp --type=hpp 2>/dev/null || echo "No matches found" echo echo "=== Checking for llvm::make_filter_range usage ===" rg -n 'make_filter_range' mlir/include/ --type=cpp --type=hpp -A 3 -B 1 | head -50Repository: munich-quantum-toolkit/core
Length of output: 311
🏁 Script executed:
#!/bin/bash set -euo pipefail FILE="mlir/include/mlir/Dialect/Utils/Utils.h" echo "=== Checking header guard ===" head -5 "$FILE" echo echo "=== Checking for `#pragma` once ===" grep -n "pragma once" "$FILE" || echo "No `#pragma` once found" echo echo "=== Verifying surrounding code context ===" cat -n "$FILE" | sed -n '1,20p'Repository: munich-quantum-toolkit/core
Length of output: 1095
🏁 Script executed:
#!/bin/bash set -euo pipefail # Search for similar std::next patterns that might have the same issue echo "=== Searching for std::next with distance in entire mlir/include ===" rg -n 'std::next\s*\(' mlir/include --type cpp -A 1 -B 1 | head -80 echo echo "=== Checking LLVM FilterRange in LLVM headers ===" find mlir -name "*.h" -o -name "*.hpp" | xargs grep -l "make_filter_range" 2>/dev/null | head -5Repository: munich-quantum-toolkit/core
Length of output: 679
🌐 Web query:
std::next iterator undefined behavior C++ standard forward iterator bounds checking💡 Result:
In C++, std::next is defined in terms of std::advance [1][2]. The C++ standard does not require std::next to perform bounds checking [3][4]. Consequently, if you use std::next to advance an iterator beyond the valid range (e.g., past the end of a container), it results in undefined behavior [3][4]. The standard library's iterator operations assume that the caller ensures the resulting iterator remains within the bounds of a valid range (specifically, that every iterator between the start and the destination, inclusive, is non-singular and valid) [5][4]. Because there is no internal bounds checking, std::next will simply perform the underlying increment operations, leading to undefined behavior if those operations violate iterator requirements [3][4]. While the C++ standard does not provide bounds checking for std::next, some specific implementations or compilation modes may provide optional "checked" iterators that trigger runtime assertions or errors in debug builds [6][7]. However, these are not guaranteed by the standard and typically do not exist in release builds to maintain performance [7]. If you need to safely advance an iterator while respecting container bounds, you should either: 1. Check the distance manually (e.g., using std::distance or container size) before calling std::next [3]. 2. Use C++20's std::ranges::next, which provides overloads that accept a sentinel (such as end) to safely bound the advancement [8][9]. This allows you to advance an iterator without exceeding the specified limit [8][10].
Citations:
- 1: https://cppreference.dev/w/cpp/iterator/next
- 2: https://dev.cppreference.com/cpp/iterator/next
- 3: https://stackoverflow.com/questions/38305559/stdnext-with-n-stddistanceit-c-end
- 4: https://stackoverflow.com/questions/5916239/stdadvance-behavior-when-advancing-beyond-end-of-container
- 5: https://eel.is/c++draft/iterator.requirements.general
- 6: https://learn.microsoft.com/en-us/cpp/standard-library/checked-iterators?view=msvc-170
- 7: https://stackoverflow.com/questions/8864429/bound-check-for-iterator-in-release-mode-c
- 8: https://cppreference.dev/w/cpp/iterator/ranges/next
- 9: https://eel.is/c++draft/iterator.primitives
- 10: https://en.cppreference.com/cpp/iterator/ranges/next
Avoid undefined iterator advancement in
getBodyUnitary.At line 254,
std::next(unitaries.begin(), i)can advance pastend()before the bounds check executes, resulting in undefined behavior. Per the C++ standard,std::nextperforms no bounds checking—advancing an iterator beyond its valid range is undefined behavior regardless of subsequent condition checks.🔧 Proposed fix
template <typename UnitaryInterface> [[nodiscard]] UnitaryInterface getBodyUnitary(Block& block, size_t i) { auto unitaries = llvm::make_filter_range( block, [](Operation& op) { return isa<UnitaryInterface>(op); }); - auto it = std::next(unitaries.begin(), static_cast<std::ptrdiff_t>(i)); + auto it = unitaries.begin(); + for (size_t n = 0; n < i && it != unitaries.end(); ++n) { + ++it; + } if (it == unitaries.end()) { llvm::reportFatalUsageError("Unitary index out of bounds"); } return cast<UnitaryInterface>(*it); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/include/mlir/Dialect/Utils/Utils.h` around lines 251 - 257, The function getBodyUnitary calls std::next to advance an iterator by i positions before checking bounds, which results in undefined behavior since std::next performs no bounds validation. To fix this, manually iterate through the filtered unitaries range while counting elements, checking if the count reaches i and if iteration completes without reaching the target index. Return the unitary operation when the count matches i, or call llvm::reportFatalUsageError if iteration completes without finding the i-th unitary.mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)
127-138: 🩺 Stability & Availability | 🔴 Critical
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # First, verify the file exists and read the function at the specified lines if [ -f "mlir/lib/Conversion/QCOToQC/QCOToQC.cpp" ]; then echo "=== File found ===" wc -l mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Function inlineRegion (lines 127-138) ===" sed -n '127,145p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp else echo "File not found" fiRepository: munich-quantum-toolkit/core
Length of output: 1082
🏁 Script executed:
#!/bin/bash # Search for the moveRegion function mentioned in the diff echo "=== Searching for moveRegion ===" fd -t f "\.cpp$|\.h$" mlir/ | xargs rg "moveRegion" -B 2 -A 5 | head -50Repository: munich-quantum-toolkit/core
Length of output: 3926
🏁 Script executed:
#!/bin/bash # Find the other call sites mentioned (821-823, 867-871, 912-919) echo "=== Checking call sites in the file ===" sed -n '821,823p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" sed -n '867,871p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" sed -n '912,919p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1082
🏁 Script executed:
#!/bin/bash # Search for all calls to inlineRegion in the file echo "=== All calls to inlineRegion ===" rg "inlineRegion\s*\(" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 2 -A 2Repository: munich-quantum-toolkit/core
Length of output: 1373
🏁 Script executed:
#!/bin/bash # Search for moveRegion in QCOToQC.cpp specifically echo "=== Searching for moveRegion in QCOToQC.cpp ===" rg "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "Exit code: $?" echo "" # Check the full context around inlineRegion calls to understand type conversion echo "=== Checking context around line 821 ===" sed -n '810,830p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" # Check class context - see if this is a pattern rewriter echo "=== Searching for class context and pattern definition ===" sed -n '1,50p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 2527
🏁 Script executed:
#!/bin/bash # Look for type conversion or adaptor usage in the region operations echo "=== Searching for adaptor usage around inlineRegion calls ===" rg -B 5 "inlineRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | head -80Repository: munich-quantum-toolkit/core
Length of output: 1985
🏁 Script executed:
#!/bin/bash # Check the QCOToJeff moveRegion implementation to understand the pattern echo "=== Full moveRegion implementation in QCOToJeff.cpp ===" rg "static LogicalResult moveRegion" mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp -A 30Repository: munich-quantum-toolkit/core
Length of output: 1421
🏁 Script executed:
#!/bin/bash # Check if there's any type conversion or adaptor in QCOToQC patterns echo "=== Searching for TypeConverter usage in QCOToQC.cpp ===" rg "TypeConverter\|getTypeConverter" mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 222
🏁 Script executed:
#!/bin/bash # Find where moveRegion is called in QCOToQC.cpp echo "=== Full context of moveRegion calls in QCOToQC.cpp ===" rg "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 10 -A 3Repository: munich-quantum-toolkit/core
Length of output: 1306
🏁 Script executed:
#!/bin/bash # Check if moveRegion is defined in QCOToQC.cpp echo "=== Search for moveRegion definition ===" rg "static.*moveRegion\|LogicalResult.*moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -A 15Repository: munich-quantum-toolkit/core
Length of output: 206
🏁 Script executed:
#!/bin/bash # Look at the complete ForOp pattern to understand type conversion concern echo "=== Complete ForOp pattern class ===" sed -n '809,850p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1554
🏁 Script executed:
#!/bin/bash # Check if there's any type conversion happening with adaptor values echo "=== Adaptor type context in patterns ===" rg "struct.*Adaptor\|class.*Adaptor\|OpAdaptor" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -A 5 | head -50Repository: munich-quantum-toolkit/core
Length of output: 206
🏁 Script executed:
#!/bin/bash # Check the return type and error handling in current inlineRegion to see if it's supposed to fail echo "=== Checking if void return is problematic ===" sed -n '120,160p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1959
🏁 Script executed:
#!/bin/bash # Find all function definitions in QCOToQC.cpp echo "=== All function definitions ===" ast-grep outline mlir/lib/Conversion/QCOToQC/QCOToQC.cpp --view expanded | grep -A 2 "moveRegion\|inlineRegion"Repository: munich-quantum-toolkit/core
Length of output: 198
🏁 Script executed:
#!/bin/bash # Search more broadly for moveRegion in the file echo "=== All occurrences of moveRegion with context ===" grep -n "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 364
🏁 Script executed:
#!/bin/bash # Check line counts around the moveRegion usages - are they before or after inlineRegion? echo "=== Lines in QCOToQC.cpp ===" wc -l mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Lines with moveRegion ===" grep -n "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Lines with inlineRegion ===" grep -n "inlineRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 979
🏁 Script executed:
#!/bin/bash # Check if moveRegion is a helper lambda or a separate function echo "=== Context around first moveRegion call (around line 1001) ===" sed -n '995,1005p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 840
🏁 Script executed:
#!/bin/bash # Look for any function that might be moveRegion before the first call echo "=== Search in the area before first moveRegion call ===" sed -n '100,200p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -A 5 -B 5 "moveRegion\|inlineRegion"Repository: munich-quantum-toolkit/core
Length of output: 1097
🏁 Script executed:
#!/bin/bash # Look at the moveRegion calls with full context echo "=== moveRegion call at line 649 ===" sed -n '640,660p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== moveRegion call at line 687 ===" sed -n '678,698p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1537
🏁 Script executed:
#!/bin/bash # Find the moveRegion function definition - it must be defined somewhere echo "=== Search for moveRegion definition in entire file ===" sed -n '1,700p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -B 5 -A 30 "moveRegion"Repository: munich-quantum-toolkit/core
Length of output: 1756
🏁 Script executed:
#!/bin/bash # Check if it's imported from a header echo "=== Check headers for moveRegion ===" head -50 mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -i "include" echo "" echo "=== Search for moveRegion in header files ===" find mlir/lib/Conversion -name "*.h" -exec grep -l "moveRegion" {} \;Repository: munich-quantum-toolkit/core
Length of output: 1282
🏁 Script executed:
#!/bin/bash # Let's look at the full section from line 640-700 to see the complete pattern echo "=== Complete pattern around moveRegion calls ===" sed -n '630,700p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 2146
🏁 Script executed:
#!/bin/bash # Search for moveRegion definition anywhere in the codebase echo "=== Search for moveRegion definition across all files ===" rg "^\s*auto moveRegion\s*=|^\s*static.*LogicalResult moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -A 20Repository: munich-quantum-toolkit/core
Length of output: 223
🏁 Script executed:
#!/bin/bash # Check if moveRegion might be a lambda defined in the pass structure echo "=== Check the full pass structure ===" sed -n '1040,1098p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 2742
🏁 Script executed:
#!/antml:bash # Search backwards from first moveRegion call to find definition echo "=== Search in lines 1-648 for moveRegion definition ===" sed -n '1,648p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | tail -100 | grep -B 20 -A 5 "moveRegion\|^static"Repository: munich-quantum-toolkit/core
Length of output: 221
🏁 Script executed:
#!/bin/bash # Let's check the include file that defines the pass echo "=== Check QCOToQC.h ===" find mlir -name "QCOToQC.h" -exec cat {} \;Repository: munich-quantum-toolkit/core
Length of output: 660
🏁 Script executed:
#!/bin/bash # Search more carefully - moveRegion might be defined as a member function of the pass echo "=== Full context of the pass class definition ===" sed -n '950,1098p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | head -150Repository: munich-quantum-toolkit/core
Length of output: 6008
🏁 Script executed:
#!/bin/bash # Search the entire repository for moveRegion definition echo "=== All moveRegion definitions in the repo ===" rg "static.*LogicalResult.*moveRegion\|auto\s+moveRegion\s*=" --type cpp --type h -B 2 -A 15Repository: munich-quantum-toolkit/core
Length of output: 212
🏁 Script executed:
#!/antml:bash # Check around the ConvertQCOCtrlOp and ConvertQCOInvOp to understand what moveRegion they're calling echo "=== Full ConvertQCOCtrlOp struct ===" sed -n '628,666p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1303
🏁 Script executed:
#!/bin/bash # Check if this conversion is actually compiling - moveRegion must be defined somewhere echo "=== Check if file builds - look for compilation artifacts ===" find . -name "CMakeLists.txt" -path "*/QCOToQC*" -exec cat {} \;Repository: munich-quantum-toolkit/core
Length of output: 1922
🏁 Script executed:
#!/bin/bash # Search all files in Conversion directory for moveRegion echo "=== All references to moveRegion in Conversion directory ===" rg "moveRegion" mlir/lib/Conversion/ --type cpp --type h | head -30Repository: munich-quantum-toolkit/core
Length of output: 1595
🏁 Script executed:
#!/bin/bash # This is key - moveRegion is called in QCOToQC.cpp but not defined. Let's verify this is a real issue echo "=== Check if QCOToQC.cpp compiles or if moveRegion is missing ===" rg "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 5 -A 2 | head -60 echo "" echo "=== Check for static moveRegion in QCOToQC.cpp ===" rg "static.*moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "Exit code: $?" echo "" echo "=== Check all helper functions defined in QCOToQC.cpp before the patterns ===" sed -n '100,650p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -B 2 -A 10 "^static\|^auto"Repository: munich-quantum-toolkit/core
Length of output: 2299
🏁 Script executed:
#!/bin/bash # Check QCToQCO.cpp to see if it has the same issue echo "=== Check QCToQCO.cpp for moveRegion definition ===" rg "static.*moveRegion\|LogicalResult moveRegion" mlir/lib/Conversion/QCToQCO/QCToQCO.cpp -A 15Repository: munich-quantum-toolkit/core
Length of output: 217
🏁 Script executed:
#!/bin/bash # Let's check the ConversionUtils header to see if moveRegion is defined there echo "=== Check ConversionUtils.h for moveRegion ===" find mlir -name "ConversionUtils.h" -exec cat {} \; | grep -B 2 -A 15 "moveRegion"Repository: munich-quantum-toolkit/core
Length of output: 851
🏁 Script executed:
#!/bin/bash # Check if there's a common header that defines moveRegion echo "=== Check all includes in QCOToQC.cpp that might have moveRegion ===" head -40 mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Look for moveRegion in those header files ===" find mlir/Conversion -name "*.h" | xargs grep -l "moveRegion" 2>/dev/nullRepository: munich-quantum-toolkit/core
Length of output: 1589
🏁 Script executed:
#!/bin/bash # Get the full ConversionUtils.h definition to understand moveRegion echo "=== Full ConversionUtils.h content ===" find mlir -name "ConversionUtils.h" -type f -exec cat {} \;Repository: munich-quantum-toolkit/core
Length of output: 1910
🏁 Script executed:
#!/bin/bash # Now verify the concern - check if SCF patterns using inlineRegion with adaptor values echo "=== Check ForOp pattern with adaptor.getInitArgs() ===" sed -n '809,835p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Check WhileOp pattern with adaptor values ===" sed -n '854,876p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Check IfOp pattern with adaptor values ===" sed -n '900,925p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 3098
🏁 Script executed:
#!/bin/bash # Verify that CtrlOp and InvOp already use moveRegion successfully echo "=== CtrlOp using moveRegion ===" sed -n '641,664p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== InvOp using moveRegion ===" sed -n '679,702p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1535
🏁 Script executed:
#!/bin/bash # Verify that adaptor values are indeed type-converted # Check how OpAdaptor works with type conversion in MLIR patterns echo "=== Understanding the issue: adaptor provides converted types ===" echo "In SCF patterns:" echo "- op.getInitArgs() returns QCO-typed values (original operation)" echo "- adaptor.getInitArgs() returns QC-typed values (type-converted)" echo "" echo "When inlineRegion is called with adaptor values:" echo "- Block args in inlined region still have QCO types" echo "- Then they're replaced with QC-typed adaptor values" echo "- This violates MLIR's same-type RAUW invariant" echo "" echo "=== Confirm the pattern is used correctly elsewhere ===" rg "inlineRegion\|moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 3 | grep -A 3 "inlineRegion\|moveRegion" | head -100Repository: munich-quantum-toolkit/core
Length of output: 638
🏁 Script executed:
#!/bin/bash # Check if there are any compile errors or test failures related to this echo "=== Check test file to understand expected behavior ===" find mlir -path "*/test*" -name "*qco_to_qc*" -type f | head -5Repository: munich-quantum-toolkit/core
Length of output: 274
🏁 Script executed:
#!/bin/bash # Verify return type consistency - inlineRegion is void but moveRegion returns LogicalResult echo "=== Function signatures comparison ===" echo "inlineRegion signature:" sed -n '127,129p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "moveRegion signature (from ConversionUtils.h):" echo "inline LogicalResult moveRegion(...)" echo "" echo "This means the proposed fix needs to:" echo "1. Change inlineRegion to return LogicalResult" echo "2. Update all callers to handle the return value"Repository: munich-quantum-toolkit/core
Length of output: 657
Update SCF region inline patterns to convert block argument types before RAUW.
inlineRegion(lines 127-138) inlines regions with original block-argument types, then replaces those args using type-converted adaptor values. This violates MLIR's same-type RAUW invariant when adaptor values have different types (e.g., QC-typed) than the block arguments (e.g., QCO-typed).The SCF conversion patterns (ForOp at 822, WhileOp at 868-870, IfOp at 913-918) pass type-converted adaptor values to
inlineRegion, triggering this issue. However, the QCO control-flow patterns (CtrlOp, InvOp) correctly usemoveRegionfromConversionUtils.hto convert block signatures during the inline operation, avoiding the invariant violation.Change
inlineRegionto use the type converter to convert block argument signatures before argument replacement, similar to themoveRegionpattern already in use. Update the function signature to returnLogicalResultand modify all call sites to handle the result.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp` around lines 127 - 138, The inlineRegion function currently inlines regions and then attempts to replace block arguments with type-converted adaptor values, violating MLIR's same-type RAUW invariant when the adaptor values have different types than the original block arguments. Modify inlineRegion to use the type converter to convert the block argument types before performing the replaceAllUsesWith operation, following the pattern used by moveRegion from ConversionUtils.h. Change the function signature to return LogicalResult instead of void to properly communicate conversion failures. Update all call sites of inlineRegion (in the ForOp pattern around line 822, WhileOp pattern around lines 868-870, and IfOp pattern around lines 913-918) to handle the returned LogicalResult.mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp (3)
343-355: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve parent modifier validity when erasing empty
inv.Line 354 removes the
InvOpeven when it is the sole body unitary of another modifier, leaving the parent body with onlyqco.yield. Use anIdOpfor the single-target nested case and bail for multi-target nested identity, matching thepow(0)handling.🛡️ Suggested nested-modifier guard
- rewriter.replaceOp(op, op.getOperands()); + if (isa<CtrlOp, InvOp, PowOp>(op->getParentOp())) { + if (op.getNumTargets() != 1) { + return failure(); + } + rewriter.replaceOpWithNewOp<IdOp>(op, op.getInputQubit(0)); + } else { + rewriter.replaceOp(op, op.getOperands()); + } return success(); } };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp` around lines 343 - 355, The EraseEmptyInv struct's matchAndRewrite method removes InvOp operations without checking if they are nested as the sole body unitary within another modifier, which violates modifier body constraints. Before the current check on op.getNumBodyUnitaries() in matchAndRewrite, add logic to detect if the InvOp is nested inside a parent modifier body and handle it accordingly: for single-target nested cases, replace with an IdOp instead of removing it; for multi-target nested cases, return failure to prevent the rewrite. This approach should mirror the handling used in the pow(0) pattern to maintain parent modifier validity.
403-430: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Require a
YieldOpbefore reading terminator operands.Line 426 dereferences
block.getTerminator()without proving the body is non-empty and yield-terminated. Malformed IR should get a verifier diagnostic instead of a null dereference or a non-yield “terminator” being accepted.🛠️ Suggested verifier guard
- auto* blockTerminator = block.getTerminator(); - if (const auto numYieldOperands = blockTerminator->getNumOperands(); + if (block.empty() || !isa<YieldOp>(block.back())) { + return emitOpError( + "last operation in body region must be a yield operation"); + } + auto yieldOp = cast<YieldOp>(block.back()); + if (const auto numYieldOperands = yieldOp->getNumOperands(); numYieldOperands != numTargets) { return emitOpError("yield operation must yield ") << numTargets << " values, but found " << numYieldOperands;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp` around lines 403 - 430, The InvOp::verify() method dereferences block.getTerminator() without first validating that the body is non-empty and that the terminator is specifically a YieldOp. Before line 426 where getTerminator() is called, add a verification check to ensure the terminator exists and is a YieldOp instance, returning an appropriate emitOpError if either condition fails. This will prevent null dereferences and reject malformed IR that contains non-yield terminators.
448-517: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Constrain matrix availability to the cases
getUnitaryMatrix()implements.Line 449 returns true for multi-target bodies with multiple known unitaries, but lines 514-515 return
std::nulloptfor those cases. Include the same zero/sole-unitary/single-target-composition conditions in the predicate.🛠️ Minimal predicate alignment
bool InvOp::hasCompileTimeKnownUnitaryMatrix() { + if (getNumBodyUnitaries() == 0) { + return true; + } + if (utils::getSoleBodyUnitary<UnitaryOpInterface>(*getBody())) { + return all_of(getBody()->getOps<UnitaryOpInterface>(), + [](UnitaryOpInterface op) { + return op.hasCompileTimeKnownUnitaryMatrix(); + }); + } + if (getNumTargets() != 1) { + return false; + } return all_of(getBody()->getOps<UnitaryOpInterface>(), [](UnitaryOpInterface op) { return op.hasCompileTimeKnownUnitaryMatrix(); }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp` around lines 448 - 517, The hasCompileTimeKnownUnitaryMatrix() function returns true for cases where getUnitaryMatrix() returns std::nullopt, causing inconsistency. Modify the hasCompileTimeKnownUnitaryMatrix() predicate to include the same conditional checks that getUnitaryMatrix() implements: it should verify that getNumBodyUnitaries() returns zero, or that there is a sole body unitary available, or that getNumTargets() equals one (for the composition case). This ensures the predicate only returns true when getUnitaryMatrix() will successfully return a matrix rather than std::nullopt.
Description
Adds a power modifier (
pow) to both MLIR-dialectsQCandQCO.The power modifier raises an operation
Uby an integer/floating numberkpow(k) { U } == U^kin other words the corresponding unitary matrix gets raised by factorkvia eigendecomposition:U = V D V† => U^p = V D^p V†.When
kis an integerU^kcould also be unrolled (Uis then appliedk-times to the input qubit - but this modifier does NOT implement any unrolling - this is left for (possibly) later stages in the pipeline).QC
QCO
Canonicalizations
The modifier contains the following canonicalizations in both dialects:
pow(a) { pow(b) { U } } → pow(a*b) { U }pow(1) { U } → Upow(0) { U } → remove (becomes identity, then eliminated)pow(-r) { U } → pow(r) { inv { U } }pow { ctrl { U } } → ctrl { pow { U } }inv:inv { pow { U } } -> pow { inf { U } }Gate specific canonicalizations
pow(r) { G(θ, ...) } → G(r·θ, ...)G in {GPhase, RX, RY, RZ, P, RXX, RYY, RZX, RZZ, R(θ,φ), XX+YY(θ,β), XX-YY(θ,β)}P-Gates:Z=P(π),S=P(π/2),Sdg=P(-π/2),T=P(π/4),Tdg=P(-π/4):pow(r) { G }→ named gate for normalizedr*base_angle, elseP(r*base_angle)All gates are implicitly converted and scaled as a P gate i.e.
G^r = P(r*base_angle). Then we check ifr*base_angleis again a base angle of a named gate: matchr*base_anglewith:0 → erase,±π → Z,π/2 → S,-π/2 → Sdg,π/4 → T,-π/4 → TdgX/Y:pow(r) { X/Y } → gphase(-r·π/2); RX/RY(r·π)Xonly:r=½ → SX,r=-½ → SXdgSX/SXdg— symmetric pair:|r| = 2 → Xpow(r) { SX } → gphase(-r·π/4); RX( r·π/2)pow(r) { SXdg } → gphase(+r·π/4); RX(-r·π/2)pow(n) { H / ECR / SWAP } → erasefor even exponentelse
→ H / ECR / SWAPfor odd exponentiSWAPpow(r) { iSWAP } → XX+YY(-r·π, 0)pow(r) { Id / Barrier } → Id / BarrierNotes
jeffdialectInvOpandPowOpis duplicated (extracting operations, reordering, etc. is basically the same) - maybe its a good idea to unify by creating utility functions, which all modifiers can use - this might be only worth doing, if more modifiers would be implemented (which is unlikely).pow(r) { inv { U }}topow(-r) { U }we leave it as is and instead convert the other way around (see Negative Power Canonicalization)Related Issues
Fixes #1129
Fixes #1131
Checklist
I have added migration instructions to the upgrade guide (if needed).If PR contains AI-assisted content:
Assisted-by: [Model Name] via [Tool Name]footer.